use std::{
    collections::HashMap,
    env,
    io::Read,
    fs::{self, File},
    path::{Path, PathBuf}
};
use crate::conf::database::*;
use crate::conf::environment::*;
use crate::conf::error::*;

pub mod environment;
pub mod database;
pub mod error;

pub const CONFIG_FILENAME: &str = "poem.toml";


#[derive(Debug)]
pub struct BasicConfig {
    pub environment: Environment,
    pub address: String,
    pub port: u16,
    pub database: Option<Database>,
    pub workers : Option<u16>,
    pub(crate) config_file_path: Option<PathBuf>,
    pub(crate) root_path : Option<PathBuf>
}

impl PartialEq for BasicConfig {
    fn eq(&self, other: &Self) -> bool {
        self.address == other.address
            && self.port == other.port
            && self.workers == other.workers
    }
}

impl BasicConfig {

    pub fn new(env: Environment) -> Self {
        Self::default(env)
    }

    pub(crate) fn default(env: Environment) -> Self {
        let default_workers = (num_cpus::get() * 2) as u16;
        let default_config = BasicConfig {
            environment: Environment::Development,
            address: "localhost".to_string(),
            port: 8000,
            database: None,
            workers: Some(default_workers),
            config_file_path: None,
            root_path: None,
        };

        match env {
            Environment::Staging => {
                BasicConfig {
                    environment: Environment::Staging,
                    ..default_config
                }
            }
            Environment::Production => {
                BasicConfig {
                    environment: Environment::Production,
                    ..default_config
                }
            }
            _ => default_config,
        }
    }

    pub fn set_root<P : AsRef<Path>>(&mut self, path: P) {
        self.root_path = Some(path.as_ref().into())
    }

    pub fn from_path<P>(env: Environment, path: P) -> Result<Self> 
        where P : AsRef<Path>
    {
        let mut config = BasicConfig::default(env);
        
        let config_file_path = path.as_ref().to_path_buf();
        if let Some(parent) = config_file_path.parent() {
            config.set_root(parent)
        } else {
            let msg = "Configuration files must be rooted in a directory";
            return Err(ConfigError::BadFilePath(config_file_path.clone(), msg.into()));
        }
        config.config_file_path = Some(config_file_path);
        Ok(config)
    }
}

#[derive(Debug, PartialEq)]
pub struct PoemConfig {
    pub active_env: Environment,
    config: HashMap<Environment, BasicConfig>
}

impl PoemConfig {

    fn find() -> Result<PathBuf> {
        let cwd = env::current_dir().map_err(|_| ConfigError::NotFound)?;
        let mut current = cwd.as_path();
        loop {
            let manifest = current.join(CONFIG_FILENAME);
            if fs::metadata(&manifest).is_ok() {
                return Ok(manifest);
            }
            match current.parent() {
                None => break,
                Some(p) => current = p,
            }
        }
        Err(ConfigError::NotFound)
    }

    pub fn get_mut(&mut self, env: Environment) -> &mut BasicConfig {
        match self.config.get_mut(&env) {
            Some(config) => config,
            None => panic!("set(): {} config is missing.", env),
        }
    }

    pub fn read_config() -> Result<PoemConfig> {
        let file = Self::find()?;
        let mut handle = File::open(&file).map_err(|_| ConfigError::IoError)?;
        let mut contents = String::new();
        handle.read_to_string(&mut contents).map_err(|_| ConfigError::IoError)?;
        PoemConfig::parse(contents, &file)
    }

    fn parse<P: AsRef<Path>>(contents: String, filename: P) -> Result<PoemConfig> {
        let path = filename.as_ref().to_path_buf();
        let table = match contents.parse::<toml::Value>() {
            Ok(toml::Value::Table(table)) => table,
            Ok(value) => {
                let err = format!("expected a table, found {}", value.type_str());
                return Err(ConfigError::ParseError(contents, path, err, Some((1, 1))));
            }
            Err(e) => return Err(ConfigError::ParseError(contents, path, e.to_string(), None))
        };
        let config = PoemConfig::active_default_from(Some(filename.as_ref()))?;
        for (entry, value) in table {
            // Each environment must be a table.
            match value.as_table() {
                None => return Err(ConfigError::BadType(
                    entry, "a table".to_string(), value.type_str().to_string(), Some(path.clone())
                )),
                Some(_) => {},
            };
        };
        Ok(config)
    }

    fn active_default_from(filename: Option<&Path>) -> Result<PoemConfig> {
        let mut defaults = HashMap::new();
        if let Some(path) = filename {
            defaults.insert(Environment::Development, BasicConfig::from_path(Environment::Development, &path)?);
            defaults.insert(Environment::Staging, BasicConfig::from_path(Environment::Staging, &path)?);
            defaults.insert(Environment::Production, BasicConfig::from_path(Environment::Production, &path)?);
        } else {
            defaults.insert(Environment::Development, BasicConfig::default(Environment::Development));
            defaults.insert(Environment::Staging, BasicConfig::default(Environment::Staging));
            defaults.insert(Environment::Production, BasicConfig::default(Environment::Production));
        }
        let config = PoemConfig {
            active_env: Environment::active()?,
            config: defaults,
        };

        Ok(config)
    }
}